View的绘制流程是Android GUI系统中的关键部分,因为最终view中绘制的内容是要呈现给用户的。本篇基于Android4.4(KitKat)将对view绘制流程做一个全面的分析。
绘制缓冲区
在View绘制流程中首先是需要一块缓冲区提供给应用程序进行内容绘制的, 这个缓冲区在上层以Surface的形式提供给使用者,这个Surface就是view绘制流程的绘图表面。在<Surface绘图缓冲区的创建流程>
一篇中我们介绍了Surface绘图缓冲区的绘制。所以关于这部分的内容不再做介绍。
绘制的时机
在<Activity启动分析(二)>一篇中,我们知道在handleResumeActivity通过addView添加view,将window加入到WMS后,会通过updateViewLayout更新视图
1 | frameworks/base/core/java/android/view/WindowManagerGlobal.java |
1 | void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) { |
在root.setLayoutParams中会通过scheduleTraversals()绘制view。view树的绘制流程是在ViewRootImpl中完成的。
绘制流程
1 | void scheduleTraversals() { |
绘制流程需要配合Vsync信号来进行,这个是通过Choreographer来进行的,在scheduleTraversals中通过mChoreographer请求同步信号,
信号到来时会调用mTraversalRunnable的doTraversal。在doTraversal中调用performTraversals来开始真正的绘制流程。
1 | private void performTraversals() { |
在performTraversals中会做很多事情,这里我们主要绘制的主要流程,即measure,layout和draw的过程,在<Surface绘图缓冲区的创建流程>一篇中我们知道了在绘制之前是需要准备画布的,这个画布就是ViewRootImpl的mSurface,它会通过relayoutWindow创建。
measure过程
measure过程主要进行view树中所有的view的大小的测量,我们看到测量并不一定会在performTraversals中进行,而是需要满足一定的条件:
- Window的状态不能为stopped,即mStopped=false
- 窗口的触摸模式发生了变化,由此引发了Activity窗口当前获焦点的控件发生了变化,即变量focusChangedDueToTouchMode的值等于true。这个检查是通过调用ensureTouchModeLocally来实现的。
- 窗口前面测量出来的宽度host.mMeasuredWidth和高度host.mMeasuredHeight不等于WindowManagerService服务计算出来的宽度mWidth和高度mHeight。这里的host为DecorView
- 窗口的内容区域边衬大小和可见区域边衬大小发生了变化, 即contentInsetsChanged的值等于true
只有满足上述条件,measure流程才会进行,在执行measure前,首先会根据DecorView的宽高获取测量规格MesaureSpec,它的前两位为mode,有三种,分别为:
- UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大
- EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
- AT_MOST:子容器可以是声明大小内的任意大小
这个测量规格会传递给子view,子view结合自身LayoutParams算出view的大小。子view测量完成后,通过setMeasureDimentions将测量结果保存。
1 | private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { |
在performMeasure之前会通过getRootMeasureSpec获取根view的MeasureSpec,它默认为屏幕的大小,
这里的mView是DecorView,它是一个FrameLayout,通过它我们来看看测量过程是如何进行的。
1 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) { |
measure方法是在view中定义的,这里需要注意measure是一个final方法,在内部它通过调用onMeasure来完成实际的测量工作。如果我们需要自定义view,就需要覆盖onMeasure在其中完成view大小的测量,我们看下默认的onMeasure实现:
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
onMeasure默认通过setMeasuredDimension设置mMeasuredWidth和mMeasuredHeight的值。通过这个方法完成测量过程。默认值是通过getDefaultSize计算得到的,它的第一个参数是通过getSuggestedMinimumxx获取到的,用来获取建议的最小宽高。
1 | protected int getSuggestedMinimumHeight() { |
这个建议的最小高度值由layout:minHeight 和 BackGround的最小高度决定,最小宽度值也是类似。getDefaultSize会通过父view传递给view的MeasureSpec来计算最终宽高。对于MeasureSpec.AT_MOST和MeasureSpec.EXACTLY两种最常见的模式,最终的大小是由specSize,也就是MeasureSpec决定的。而不是由建议值。
layout过程
在测量完view大小后,通过layout来确定view的位置,layout流程需满足下面的条件:
1 | final boolean didLayout = layoutRequested && !mStopped; |
- 通过requestLayout发起过layout请求
- Window的状态不为stopped
1 | private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, |
同样是从view根部开始,我们看看DecorView的layout实现
1 | frameworks/base/core/java/android/view/ViewGroup.java |
如果LayoutTransitiond动画未执行,那么直接调用View的layout,否则设置mLayoutCalledWhileSuppressed = true,等待动画完成后再进行requestyLayout。
1 | public void layout(int l, int t, int r, int b) { |
layout的布局是通过onLayout来实现的,在View中onLayout的实现是空的实现,因为view是通过其父view即viewGroup进行layout的
1 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
ViewGroup的onLayout,它是个抽象方法,所有ViewGroup的子类都需要实现此方法以完成其子view的布局。
1 | protected abstract void onLayout(boolean changed, |
我们通过以LinearLayout为例说明ViewGroup是如何进行子view的布局的。
1 |
|
根据不同的oriention来调用不同的布局方法,这里我们看看layoutVertical。
1 | void layoutVertical(int left, int top, int right, int bottom) { |
draw过程
1 | private void performTraversals() { |
draw流程需要满足:
- 未取消绘制也没有创建新的Surface,dispatchOnPreDraw返回true或者View不可见则取消绘制
- 未跳过此次绘制,即skipDraw为false,或者mReportNextDraw为true
1 | private void performDraw() { |
performDraw中会调用draw方法来完成绘制,draw方法中会通过attachInfo.mHardwareRenderer来判断是否启用了硬件加速,关于硬件加速我们在后面的篇幅进行讨论,这里我们假设未开启硬件加速关注软件绘制,即drawSoftware这个方法,这个方法首先会通过mSurface来取得绘制得画布canvas,并以dirty作为裁剪区域进行view绘制,最后通过unlockCanvasAndPost提交绘制得结果。这里mView就是我们的DecorView
1 | frameworks/base/core/java/android/view/View.java |
绘制流程的代码有点长,我做了稍微精简了下,从注释中我们可以看到绘制的具体流程分为6步:
- 绘制背景(Draw the background)
- 如果需要,为view的淡隐淡出效果保存layers(If necessary, save the canvas’ layers to prepare for fading)
- 绘制view的内容(Draw view’s content)
- 绘制子view(Draw children)
- 如果需要,绘制淡隐淡出效果并恢复保存的layers If necessary, draw the fading edges and restore layers
- 绘制装饰内容,比如滚动条等 Draw decorations (scrollbars for instance)
draw的第一步是进行view背景的绘制,但并不是必须的,背景资源存放在mBackground中,scrollX和scrollY都为0说明该view不用滚动,可以直接绘制其背景,否则需要进行坐标转换。一般情况下view是不带fading效果的,这时候就不需要进行第2步和第5步,否则就需要进行2到6的所有步骤。绘制view的content是通过onDraw来实现的,即真正的内容是通过子类来进行绘制的。因为view的onDraw是个空实现。绘制好自身的内容后可能该view还有子view,这时候就需要通过dispatchDraw来通知子view进行绘制,记住我们绘制的流程是从decorView开始的,它是个ViewGroup,也是整个view树的根view。我们知道ViewGroup是所有父容器的父类,所以dispatchDraw放在其中实现最合适不过了。
1 |
|
这里主要遍历父view的所有子view通过drawChild来进行绘制。在drawChild中又回到我们view的draw绘制流程,其中会通过onDraw来进行child view的内容的绘制。整个绘制流程就是这样的。